package com.celesea.configuration.log;

import org.apache.commons.io.output.TeeOutputStream;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
import java.io.*;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.net.URLDecoder;
import java.nio.charset.Charset;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Enumeration;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * 过滤器 for log
 *合并om.calm.b.scm.framework.common.filter.LogFiler
 * @author Administrator
 */
public class LogFilter extends ExcludableFilter {

    public static final String KEY_KRY_GLOBAL_MSG_ID = "_KRY_GLOBAL_MSG_ID";
    public static final String HEADER_KEY_KRY_GLOBAL_MSG_ID = "_kry_global_msg_id";

    public static final String IP_MDC_KEY = "IP";
    public static final String URL_KEY = "URL";
    public static final String HTTP_METHOD_KEY = "HTTP_METHOD";

    public static final String ID_FORMAT = "%s-%s-%s";

    public static final char Separator = '@';
    private static final Logger logger = LoggerFactory.getLogger(LogFilter.class);
    private static final Logger RequestLogger = LoggerFactory.getLogger("Request");
    private static AtomicInteger id = new AtomicInteger(1);
    protected String ip;
    protected String shortIP;
    
    public static final int MAX_REQUEST_SIZE = 512;
    public static final int MAX_RESPONSE_SIZE = 1024;
    
    //SimpleDateFormat线程不安全
    private static final ThreadLocal<SimpleDateFormat> sdf = new ThreadLocal<SimpleDateFormat>(){
    	protected SimpleDateFormat initialValue() {
    		return new SimpleDateFormat("yyMMddHH");
    	};
    };

    private String getId() {
        String s = sdf.get().format(new Date());
        long requestId = id.incrementAndGet();

        return String.format(ID_FORMAT, shortIP, s, requestId);
    }

    /**
     * 执行具体的过滤操作
     */
    @Override
	public void doInternalFilter(ServletRequest request,
			ServletResponse response, FilterChain chain) throws IOException,ServletException {
    	MDC.put(IP_MDC_KEY, ip);
        HttpServletRequest req = (HttpServletRequest) request;
        HttpServletResponse res = (HttpServletResponse) response;
        
        MDC.put("remoteIp", getRemoteIp(req));
		MDC.put("localIp", getLocalIP());
        
        String kryGlobalMsgId = req.getHeader(HEADER_KEY_KRY_GLOBAL_MSG_ID);
        if (StringUtils.isBlank(kryGlobalMsgId)) {
            kryGlobalMsgId = getId();
        }
        MDC.put(KEY_KRY_GLOBAL_MSG_ID, kryGlobalMsgId);

        long startNano = System.nanoTime();

        MDC.put(URL_KEY, req.getRequestURI());

        String method = req.getMethod().toUpperCase();
        MDC.put(HTTP_METHOD_KEY, method);

        StringBuffer logBuffer = new StringBuffer();
        logRequestParameters(logBuffer, req);
        
        BufferedRequestWrapper wrappedRequest = new BufferedRequestWrapper(req);
        logRequestBody(logBuffer, wrappedRequest);
        ResponseWrapper responseWrapper = new ResponseWrapper(res);
        try {
            chain.doFilter(wrappedRequest, responseWrapper);
        } finally {
            long endNano = System.nanoTime();
            long ms = (endNano - startNano) / 1000000;

            String contentType = responseWrapper.getContentType();// responseWrapper.getHeader("Content-Type");
            String contentEncoding = responseWrapper.getHeader("Content-Encoding");

            if (contentType == null) contentType = "";
            if (contentEncoding == null) contentEncoding = "";
            boolean isLogRes = !contentType.contains("application/gzip") && !contentEncoding.contains("gzip");
            if (isLogRes) logResponse(logBuffer, responseWrapper, ms);
            RequestLogger.info(logBuffer.toString());
            MDC.remove(URL_KEY);
            MDC.remove(KEY_KRY_GLOBAL_MSG_ID);
            MDC.remove(HTTP_METHOD_KEY);
            MDC.remove(IP_MDC_KEY);
        }
    }

	private void logRequestParameters(StringBuffer logBuffer,
			HttpServletRequest request) throws IOException {
        logBuffer.append("RequestParam:[");
        Enumeration<String> en = request.getParameterNames();
        
        String key = null;
        String value = null;
        while (en.hasMoreElements()) {
            key = en.nextElement();
            if (StringUtils.isNotBlank(key)) {
            	value = String.join("|", request.getParameterValues(key));
            	
                logBuffer.append("&" + key + "=" + value);
            }
        }

        logBuffer.append("], ");
    }

	private void logRequestBody(StringBuffer logBuffer, BufferedRequestWrapper request) throws IOException {
        String contentType = request.getContentType();
        if (contentType == null) {
            contentType = "";
        }
        
        logBuffer.append("size:");
        if(request.isNotNull()){
        	logBuffer.append(request.size());
        }else{
        	logBuffer.append(0);
        }
        logBuffer.append(" bytes");

        logBuffer.append(", RequestBody:[");
        if(request.isNotNull()){
        	String requestBody = null;
        	if (contentType.contains("application/x-www-form-urlencoded") && request.size() < MAX_REQUEST_SIZE) {
                requestBody = new String(request.toByteArray(), Constant.DEFAULT_CHARSET_STR);
                logBuffer.append(URLDecoder.decode(requestBody, Constant.DEFAULT_CHARSET_STR)
                                         .replaceAll("\n|\r|  ", ""));
            }else{
           	 	 boolean flag = contentType.contains("application/json") || contentType.contains("application/xml") ||
                		 contentType.contains("multipart/form-content") || contentType.contains("text/plain") || contentType.contains("text/xml");
                 if (flag && request.size() < MAX_REQUEST_SIZE) {
                     requestBody = new String(request.toByteArray(), Constant.DEFAULT_CHARSET_STR);
                     logBuffer.append(requestBody.replaceAll("\n|\r| ", ""));
                 }else{
               	     logBuffer.append("request not log,contentType is ");
              	     logBuffer.append(contentType);
                 }
            }
        }
        logBuffer.append("], ");
    }

    private void logResponse(StringBuffer logBuffer, ResponseWrapper response, long ms) {
        logBuffer
        		.append("Status:")
        		.append(response.getStatus())
        		.append(", ")
                .append("Spent:")
                .append(ms)
                .append("ms, ");
        try {
            logBuffer.append("size:")
                    .append(response.size())
                    .append(" bytes");
            
            String contentType = response.getContentType();
            if (contentType == null) {
                contentType = "";
            }
            boolean flag = contentType.contains("multipart/form-content") || contentType.contains("application/json")
    				|| contentType.contains("application/xml") || contentType.contains("text/plain") || contentType.contains("text/xml");
    		logBuffer.append(", ResponseBody:[");
    		if(flag && response.size() < MAX_RESPONSE_SIZE){
                String content = new String(response.toByteArray(), Constant.DEFAULT_CHARSET_STR).replaceAll("\n|\r", "");
                logBuffer.append(content);
            }else{
            	logBuffer.append("response not log,contentType is ");
            	logBuffer.append(contentType);
            }
    		logBuffer.append("]");
        } catch (UnsupportedEncodingException e) {
            logger.warn("Failed mongo parse resource payload", e);
        }
    }

    /**
	 * Destroy method for this filter
	 *
	 */
	@Override
	public void destroy() {
		logger.debug("LogFilter Destroy====================");
	}

	/**
	 * Init method for this filter
	 *
	 */
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
    	logger.debug("LogFilter Init====================");
        NetworkHelper.IpAddress ipAddress = NetworkHelper.getIp();
        this.ip = ipAddress.getLocalInetAddress().getHostAddress();
        this.shortIP = NetworkHelper.getShortIP(ip);
        super.init(filterConfig);
    }

    
    private static final class BufferedRequestWrapper extends HttpServletRequestWrapper {

        private ByteArrayInputStream bais = null;
        private ByteArrayOutputStream baos = null;
        private BufferedServletInputStream bsis = null;
        private byte[] buffer = null;
        private ServletInputStream origin;


        public BufferedRequestWrapper(HttpServletRequest req) throws IOException {
            super(req);
//            this.id = requestId;
            // Read InputStream and store its content in a buffer.
            origin = req.getInputStream();
            this.baos = new ByteArrayOutputStream();
            if (origin != null) {
                byte buf[] = new byte[1024];
                int letti;
                while ((letti = origin.read(buf)) > 0) {
                    this.baos.write(buf, 0, letti);
                }
                this.buffer = this.baos.toByteArray();
            }
        }


        @Override
        public ServletInputStream getInputStream() {
            if (this.buffer == null || this.origin == null) return null;
            if (this.bsis != null) return this.bsis;
            this.bais = new ByteArrayInputStream(this.buffer);
            this.bsis = new BufferedServletInputStream(this.origin, this.bais);
            return this.bsis;
        }

        public byte[] toByteArray() {
            return buffer;
        }
        
        public int size(){
        	return buffer != null?buffer.length:0;
        }
		
		public boolean isNotNull(){
			return buffer != null;
		}
    }


    private static final class BufferedServletInputStream extends ServletInputStream {

        private ServletInputStream origin;

        private ByteArrayInputStream bais;

        public BufferedServletInputStream(ServletInputStream origin, ByteArrayInputStream bais) {
            this.origin = origin;
            this.bais = bais;
        }

        @Override
        public int available() {
            return this.bais.available();
        }

        @Override
        public int read() {
            return this.bais.read();
        }

        @Override
        public int read(byte[] buf, int off, int len) {
            return this.bais.read(buf, off, len);
        }

        @Override
        public boolean isFinished() {
            return origin.isFinished();
        }

        @Override
        public boolean isReady() {
            return origin.isReady();
        }

        @Override
        public void setReadListener(ReadListener readListener) {
            origin.setReadListener(readListener);
        }
    }

    private static class ResponseWrapper extends HttpServletResponseWrapper {
        private final ByteArrayOutputStream bos = new ByteArrayOutputStream();
        private PrintWriter writer;

        public ResponseWrapper(HttpServletResponse response) {
            super(response);
            this.writer = new PrintWriter(new OutputStreamWriter(this.bos, Constant.DEFAULT_CHARSET));
        }

        public ServletResponse getResponse() {
            return this;
        }

        public ServletOutputStream getOutputStream() throws IOException {
            return new ServletOutputStream() {
                ServletOutputStream origin = ResponseWrapper.super.getOutputStream();
                private TeeOutputStream tee;

                {
                    this.tee = new TeeOutputStream(ResponseWrapper.super.getOutputStream(), ResponseWrapper.this.bos);
                }

                public boolean isReady() {
                    return this.origin.isReady();
                }

                public void setWriteListener(WriteListener writeListener) {
                    this.origin.setWriteListener(writeListener);
                }

                public void write(int b) throws IOException {
                    this.tee.write(b);
                }
            };
        }

        public PrintWriter getWriter() throws IOException {
            return new TeePrintWriter(super.getWriter(), this.writer);
        }

        public byte[] toByteArray() {
            return this.bos.toByteArray();
        }
        
        public int size(){
        	return this.bos.size();
        }
    }

    
    private String getLocalIP() {

		String sIP = "";
		InetAddress ip = null;
		try {
			boolean bFindIP = false;
			Enumeration<NetworkInterface> netInterfaces = NetworkInterface.getNetworkInterfaces();
			while (netInterfaces.hasMoreElements()) {
				if (bFindIP) {
					break;
				}
				NetworkInterface ni = netInterfaces.nextElement();
				Enumeration<InetAddress> ips = ni.getInetAddresses();
				while (ips.hasMoreElements()) {
					ip = ips.nextElement();
					if (!ip.isLoopbackAddress() && ip.getHostAddress().matches("(\\d{1,3}\\.){3}\\d{1,3}")) {
						bFindIP = true;
						break;
					}
				}
			}
		} catch (Exception e) {
			logger.error("ERROR ", e);
		}
		if (ip != null) {
			sIP = ip.getHostAddress();
		}
		return sIP;
	}

	private String getRemoteIp(HttpServletRequest req){

		String remoteAddr = req.getRemoteAddr();

		String forwarded = req.getHeader("X-Forwarded-For");

		String realIp = req.getHeader("X-Real-IP");

		String ip;
		if(realIp == null) {
			ip = forwarded == null ? remoteAddr : remoteAddr + "/" + forwarded.split(",")[0];
		} else {
			ip = realIp.equals(forwarded) ? realIp : (forwarded != null ? realIp + "/" + forwarded.split(",")[0] : realIp);
		}
		return ip;
	}

    public interface Constant {
        String DEFAULT_CHARSET_STR = "UTF-8";
        Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
    }


}
